<?php
namespace Core\Common\Helper;

use Core\Common\Component\Cache\CacheStore;
use Illuminate\Support\Arr;

/**
 * 缓存访问的封装
 * Class CacheLoader
 * @package App\Helper
 */
class CacheLoader
{
    /**
     * @var CacheStore
     */
    protected $cache;

    /**
     * @var array
     */
    protected $options;

    /**
     * CacheLoader constructor.
     * @param CacheStore $cache
     * @param array $options
     */
    public function __construct($cache, $options)
    {
        $this->cache = $cache;

        $this->options = $options + [
            'prefix' => '',             // 缓存前缀，默认无前缀
            'ttl' => 86400,             // 缓存时长，默认24小时，-1为不过期
            'back_to_source' => true,   // 如果缓存中没有，是否回源,
            'cache_null_value' => true, // 是否缓存null值
            'tags' => [],               // 如果设置了tags，则会将键值按组划分，便于按组清除缓存
            'validator' => null         // 如果设置了validator(回调函数)，则从缓存取出的数据会通过这个函数验证
        ];
    }

    /**
     * 设置option
     * @param string $key
     * @param mixed $value
     */
    public function setOption($key, $value)
    {
        $this->options[$key] = $value;
    }

    /**
     * 获取缓存键
     * @param string $prefix
     * @param string $key
     * @return string
     */
    protected function cacheKey($prefix, $key)
    {
        return "{$prefix}{$key}";
    }

    /**
     * @return CacheStore
     */
    protected function getCache()
    {
        $tags = $this->options['tags'];
        if (empty($tags)) {
            return $this->cache;
        }
        return $this->cache->tags($tags);
    }

    protected function validate($value, $options)
    {
        $validator = $options['validator'];
        if ($validator) {
            return call_user_func($validator, $value);
        }
        return ($value !== false);
    }

    /**
     * @param string[] $keys
     * @param string $prefix
     * @return mixed[]
     */
    protected function mGetWithPrefix($keys, $prefix)
    {
        if (empty($prefix)) {
            return $this->getCache()->mGet($keys);
        }

        $cKeys = array_map(function($key) use ($prefix) {
            return $this->cacheKey($prefix, $key);
        }, $keys);

        return $this->getCache()->mGet($cKeys);
    }

    /**
     * @param string[] $items
     * @param string $prefix
     * @param int $ttl
     */
    protected function mSetWithPrefix($items, $prefix, $ttl)
    {
        $cacheItems = [];
        foreach ($items as $k => $v) {
            $k = $this->cacheKey($prefix, $k);
            $cacheItems[$k] = $v;
        }
        $this->getCache()->mSet($cacheItems, $ttl);
    }

    /**
     * 按key查询缓存，没有则调用callback
     * @param string $key
     * @param callable $missLoader
     * @param array $options
     * @return mixed
     */
    public function cached($key, $missLoader, $options = [])
    {
        $options = $options + $this->options;

        $cKey = $this->cacheKey($options['prefix'], $key);

        $value = $this->getCache()->get($cKey);
        if (!$this->validate($value, $options) && $options['back_to_source']) {
            $value = call_user_func($missLoader, $key);
            if ($value === false) {
                return false;
            }
            if ($options['cache_null_value'] || !is_null($value)) {
                $this->getCache()->set($cKey, $value, $options['ttl']);
            }
        }

        return $value;
    }

    /**
     * 按key的数组查询，缓存，没有则调用callback
     * @param string[] $keys
     * @param callable $missLoader 返回的数组需要和传入的keys下标对应，没有的数据用null占位
     * @param array $options
     * @return mixed[]
     */
    public function cachedMulti($keys, $missLoader, $options = [])
    {
        $options = $options + $this->options;
        $keys = array_values($keys);
        $cResults = $this->mGetWithPrefix($keys, $options['prefix']);
        $missed = [];
        $missedIndexes = [];
        foreach ($cResults as $i => $cResult) {
            if ($this->validate($cResult, $options)) {
                continue;
            }

            if (!$options['back_to_source']) {
                $cResults[$i] = null;
            } else {
                $missed[] = $keys[$i];
                $missedIndexes[count($missed)-1] = $i;
            }
        }

        if (!empty($missed)) {
            $items = call_user_func($missLoader, $missed);
            $cacheItems = [];
            $n = count($items);

            foreach ($missed as $j => $missedKey) {
                $item = $j < $n ? $items[$j] : null;
                if ($options['cache_null_value'] || !is_null($item)) {
                    $cacheItems[$missedKey] = $item;
                }
                $cResults[$missedIndexes[$j]] = $item;
            }

            if (!empty($cacheItems)) {
                $this->mSetWithPrefix($cacheItems, $options['prefix'], $options['ttl']);
            }
        }
        return $cResults;
    }

    /**
     * 覆盖指定key的缓存
     * @param string $key
     * @param mixed $value
     * @param array $options
     */
    public function set($key, $value, $options = [])
    {
        $options = $options + $this->options;

        $key = $this->cacheKey($options['prefix'], $key);

        $this->getCache()->set($key, $value, $options['ttl']);
    }

    /**
     * 覆盖指定的一堆key的缓存
     * @param array $items
     * @param array $options
     */
    public function setMulti($items, $options = [])
    {
        $options = $options + $this->options;

        $this->mSetWithPrefix($items, $options['prefix'], $options['ttl']);
    }

    /**
     * 清除某个key的缓存
     * @param string $key
     * @param array $options
     */
    public function forget($key, $options = [])
    {
        $options = $options + $this->options;

        $key = $this->cacheKey($options['prefix'], $key);

        $this->getCache()->del($key);
    }

    /**
     * 清除指定keys的缓存
     * @param array $keys
     * @param array $options
     */
    public function forgetMulti($keys, $options = [])
    {
        $options = $options + $this->options;

        $keys = array_map(function($key) use ($options) {
            return $this->cacheKey($options['prefix'], $key);
        }, $keys);

        $this->getCache()->mDel($keys);
    }

    /**
     * 清空带tag的缓存
     * @param string[] $tags
     */
    public function flushTaggedCache($tags)
    {
        $tags = $tags ?: Arr::get($this->options, 'tags', []);

        if (empty($tags)) {
            return;
        }
        $this->cache->tags($tags)->flush();
    }
}